# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

GEN_DIR             := $(realpath ../../../vendor/google_riscv-dv)
TOOLCHAIN           := ${RISCV_TOOLCHAIN}

# Explicitly ask for the bash shell
SHELL                := bash

# Seed for instruction generator and RTL simulation
#
# By default, SEED is set to a different value on each run by picking a random
# value in the Makefile. For overnight testing, a sensible seed might be
# something like the output of "date +%y%m%d". For regression testing, you'll
# need to make sure that a the seed for a failed test "sticks" (so we don't
# start passing again without fixing the bug).
SEED                := $(shell echo $$RANDOM)

# This is the top-level output directory. Everything we generate goes in
# here. Most generated stuff actually goes in $(OUT)/seed-$(SEED), which allows
# us to run multiple times without deleting existing results.
OUT                 := out
OUT-SEED            := $(OUT)/seed-$(SEED)

# Run time options for the instruction generator
GEN_OPTS            :=
# Compile time options for ibex RTL simulation
COMPILE_OPTS        +=
# Run time options for ibex RTL simulation
SIM_OPTS            :=
# Enable waveform dumping
WAVES               := 1
# Enable coverage dump
COV                 := 0
# RTL simulator
SIMULATOR           := vcs
# ISS (spike, ovpsim)
ISS                 := ovpsim
# ISS runtime options
ISS_OPTS            :=
# ISA
ISA                 := rv32imc
# Test name (default: full regression)
TEST                := all
TESTLIST            := riscv_dv_extension/testlist.yaml
# Verbose logging
VERBOSE             :=
# Number of iterations for each test, assign a non-zero value to override the
# iteration count in the test list
ITERATIONS          := 0
# LSF CMD
LSF_CMD             :=
# Generator timeout limit in seconds
TIMEOUT             := 1800
# Privileged CSR YAML description file
CSR_FILE            := riscv_dv_extension/csr_description.yaml
# Pass/fail signature address at the end of test
SIGNATURE_ADDR      := 8ffffffc

### Ibex top level parameters ###
### Required by RISCV-DV, some ISS, and RTL ###
# PMP Regions
PMP_REGIONS         := 16
# PMP Granularity
PMP_GRANULARITY     := 0

# TODO(udinator) - might need options for SAIL/Whisper/Spike
ifeq (${ISS},ovpsim)
	ISS_OPTS += --override riscvOVPsim/cpu/PMP_registers=${PMP_REGIONS}
	ISS_OPTS += --override riscvOVPsim/cpu/PMP_grain=${PMP_GRANULARITY}
endif

# Check which simulator is being used and add correct compile options
ifeq (${SIMULATOR},vcs)
	COMPILE_OPTS += -pvalue+core_ibex_tb_top.dut.PMPNumRegions=${PMP_REGIONS}
	COMPILE_OPTS += -pvalue+core_ibex_tb_top.dut.PMPGranularity=${PMP_GRANULARITY}
else ifeq (${SIMULATOR},ius)
	COMPILE_OPTS += -defparam core_ibex_tb_top.dut.PMPNumRegions=${PMP_REGIONS}
	COMPILE_OPTS += -defparam core_ibex_tb_top.dut.PMPGranularity=${PMP_GRANULARITY}
else ifeq (&{SIMULATOR},riviera)
	SIM_OPTS +=-g/core_ibex_tb_top/dut/PMPNumRegions=${PMP_REGIONS}
	SIM_OPTS +=-g/core_ibex_tb_top/dut/PMPGranularity=${PMP_GRANULARITY}
# TODO(udinator) - support dsim
endif

SHELL=/bin/bash

export PRJ_DIR:= $(realpath ../../../..)

all: sim

instr: iss_sim

sim: post_compare cov

.PHONY: clean
clean:
	rm -rf ${OUT}

# Common options for all targets
COMMON_OPTS := $(if $(call equal,$(VERBOSE),1),--verbose,)

# Options for all targets that depend on the tests we're running.
TEST_OPTS := $(COMMON_OPTS) \
             --seed=${SEED} \
             --test="${TEST}" \
             --testlist=${TESTLIST} \
             --iterations=${ITERATIONS}

# Options used for privileged CSR test generation
CSR_OPTS=--csr_yaml=${CSR_FILE} \
         --isa="${ISA}" \
         --end_signature_addr=${SIGNATURE_ADDR}

RISCV_DV_OPTS=--custom_target=riscv_dv_extension \
              --isa="${ISA}" \
              --mabi=ilp32 \

# To avoid cluttering the output directory with stamp files, we place them in
# $(metadata).
metadata := $(OUT-SEED)/.metadata

# This is a list of directories that are automatically generated by some
# targets. To ensure the directory has been built, add a order-only dependency
# (with the pipe symbol before it) on the directory name and add the directory
# to this list.
gen-dirs := $(OUT) $(OUT-SEED) $(metadata) $(OUT)/rtl_sim

$(gen-dirs): %:
	mkdir -p $@

###############################################################################
# Utility functions.
#
# If VS is a list of variable names, P is a path and X is a string, then $(call
# dump-vars,P,X,VS) will expand to a list of 'file' commands that write each
# variable to P in Makefile syntax, but with "last-X-" prepended. At the start
# of the file, we also define last-X-vars-loaded to 1. You can use this to
# check whether there was a dump file at all.
#
# Note that this doesn't work by expanding to a command. Instead, *evaluating*
# dump-vars causes the variables to be dumped.
dump-var  = $(file >>$(1),last-$(2)-$(3) := $($(3)))
dump-vars = $(file >$(1),last-$(2)-vars-loaded := .) \
            $(foreach name,$(3),$(call dump-var,$(1),$(2),$(name)))

# equal checks whether two strings are equal, evaluating to '.' if they are and
# '' otherwise.
both-empty = $(if $(1),,$(if $(2),,.))
find-find = $(if $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))),.,)
equal = $(or $(call both-empty,$(1),$(2)),$(call find-find,$(1),$(2)))

# var-differs is used to check whether a variable has changed since it was
# dumped. If it has changed, the function evaluates to '.' (with some
# whitespace) and prints a message to the console; if not, it evaluates to ''.
#
# Call it as $(call var-differs,X,TGT,V).
var-differs = \
  $(if $(call equal,$(strip $($(3))),$(strip $(last-$(1)-$(3)))),,\
       .$(info Repeating $(2) because variable $(3) has changed value.))

# vars-differ is used to check whether several variables have the same value as
# they had when they were dumped. If we haven't loaded the dumpfile, it
# silently evaluates to '!'. Otherwise, if all the variables match, it
# evaluates to '.'. If not, it evaluates to '.' and prints some messages to the
# console explaining why a rebuild is happening.
#
# Call it as $(call vars-differ,X,TGT,VS).
vars-differ-lst = $(foreach v,$(3),$(call var-differs,$(1),$(2),$(v)))
vars-differ-sp = \
  $(if $(last-$(1)-vars-loaded),\
       $(if $(strip $(call vars-differ-lst,$(1),$(2),$(3))),.,),\
       !)
vars-differ = $(strip $(call vars-differ-sp,$(1),$(2),$(3)))

# A phony target which can be used to force recompilation.
.PHONY: FORCE
FORCE:

# vars-prereq is empty if every variable in VS matches the last run (loaded
# with tag X), otherwise it is set to FORCE (which will force a recompile and
# might print a message to the console explaining why we're rebuilding TGT).
#
# Call it as $(call vars-prereq,X,TGT,VS)
vars-prereq = $(if $(call vars-differ,$(1),$(2),$(3)),FORCE,)

###############################################################################
# Generate random instructions
#
# This depends on the vendored in code in $(GEN_DIR). It also depends on the
# values of some variables (we want to regenerate things if, for example, the
# simulator changes). Since we're writing out to $(OUT-SEED), we don't have to
# depend on the value of SEED. However, we do have to make sure that the
# variables whose names are listed in $(gen-var-deps) haven't changed.
#
# To do this variable tracking, we dump each of the variables to a Makefile
# fragment and try to load it up the next time around.
gen-var-deps := GEN_OPTS SIMULATOR RISCV_DV_OPTS CSR_OPTS \
	            SIGNATURE_ADDR PMP_REGIONS PMP_GRANULARITY TEST_OPTS

# Load up the generation stage's saved variable values. If this fails, that's
# no problem: we'll assume that the previous run either doesn't exist or
# something went wrong.
-include $(metadata)/gen-vars.mk

# gen-vars-prereq is empty if every variable in gen-var-deps matches the last run,
# otherwise it is set to FORCE (which will force a recompile). Note that we
# define it with '=', not ':=', so we don't evaluate it if we're not trying to
# run the gen target.
gen-vars-prereq = \
  $(call vars-prereq,gen,building instruction generator,$(gen-var-deps))

# A variable containing a file list for the riscv-dv vendored-in module.
# Depending on these files gives a safe over-approximation that will ensure we
# rebuild things if that module changes.
#
# Note that this is defined with ":=". As a result, we'll always run the find
# command exactly once. Wasteful if we're trying to make clean, but much better
# than running it for every target otherwise.
risc-dv-files := $(shell find $(GEN_DIR) -type f)

# This actually runs the instruction generator. Note that the rule depends on
# the (phony) FORCE target if any variables have changed. If the rule actually
# runs, it starts by deleting any existing contents of $(OUT-SEED)/instr_gen.
$(metadata)/instr_gen.gen.stamp: \
  $(gen-vars-prereq) $(risc-dv-files) $(TESTLIST) | $(metadata)
	@rm -rf $(OUT-SEED)/instr_gen
	@python3 ${GEN_DIR}/run.py \
     --output=$(OUT-SEED)/instr_gen ${GEN_OPTS} \
     --steps=gen \
     --gen_timeout=${TIMEOUT} \
     --lsf_cmd="${LSF_CMD}" \
     --simulator="${SIMULATOR}" \
     ${RISCV_DV_OPTS} \
     ${TEST_OPTS} \
     ${CSR_OPTS} \
     --sim_opts="+uvm_set_inst_override=riscv_asm_program_gen,ibex_asm_program_gen,"uvm_test_top.asm_gen" \
                 +signature_addr=${SIGNATURE_ADDR} +pmp_num_regions=${PMP_REGIONS} \
                 +pmp_granularity=${PMP_GRANULARITY}"
	$(call dump-vars,$(metadata)/gen-vars.mk,gen,$(gen-var-deps))
	@touch $@

.PHONY: gen
gen: $(metadata)/instr_gen.gen.stamp

###############################################################################
# Compile the generated assembly programs
#
# We don't explicitly track dependencies on the RISCV toolchain, so this
# doesn't depend on anything more than the instr_gen stage did.
$(metadata)/instr_gen.compile.stamp: \
  $(metadata)/instr_gen.gen.stamp $(TESTLIST)
	@python3 ${GEN_DIR}/run.py \
     --o=$(OUT-SEED)/instr_gen ${GEN_OPTS} \
     --steps=gcc_compile \
     ${TEST_OPTS} \
     --gcc_opts=-mno-strict-align \
     ${RISCV_DV_OPTS} && \
	  touch $@

.PHONY: gcc_compile
gcc_compile: $(metadata)/instr_gen.compile.stamp

###############################################################################
# Run the instruction set simulator
#
# This (obviously) depends on having compiled the generated programs, so we
# don't have to worry about variables that affect the 'gen' stage. However, the
# ISS and ISS_OPTS variables do affect the output, so we need to dump them. See
# the 'gen' stage for more verbose explanations of how this works.
iss-var-deps := ISS ISS_OPTS
-include $(metadata)/iss-vars.mk
iss-vars-prereq = $(call vars-prereq,iss,running ISS,$(iss-var-deps))

$(metadata)/instr_gen.iss.stamp: \
  $(iss-vars-prereq) $(TESTLIST) $(metadata)/instr_gen.compile.stamp
	@python3 ${GEN_DIR}/run.py \
     --o=$(OUT-SEED)/instr_gen ${GEN_OPTS} \
     --steps=iss_sim \
     ${TEST_OPTS} \
     --iss="${ISS}" \
     --iss_opts="${ISS_OPTS}" \
     ${RISCV_DV_OPTS}
	$(call dump-vars,$(metadata)/iss-vars.mk,iss,$(iss-var-deps))
	@touch $@

.PHONY: iss_sim
iss_sim: $(metadata)/instr_gen.iss.stamp


###############################################################################
# Compile ibex core TB
#
# Note that (unlike everything else) this doesn't depend on the seed: the DUT
# doesn't depend on which test we're running!
#
# It does, however, depend on various variables. These are listed in
# compile-var-deps. See the 'gen' stage for more verbose explanations of how
# the variable dumping works.
#
# The compiled ibex testbench (obviously!) also depends on the design and the
# DV code. The clever way of doing this would be to look at a dependency
# listing generated by the simulator as a side-effect of doing the compile (a
# bit like using the -M flags with a C compiler). Unfortunately, that doesn't
# look like it's particularly easy, so we'll just depend on every .v, .sv or
# .svh file in the dv or rtl directories. Note that this variable is set with
# '=', rather than ':='. This means that we don't bother running the find
# commands unless we need the compiled testbench.
all-verilog = \
  $(shell find ../../../rtl -name '*.v' -o -name '*.sv' -o -name '*.svh') \
  $(shell find ../.. -name '*.v' -o -name '*.sv' -o -name '*.svh')

compile-var-deps := COMMON_OPTS SIMULATOR COV WAVES COMPILE_OPTS
-include $(OUT)/rtl_sim/.compile-vars.mk
compile-vars-prereq = $(call vars-prereq,comp,compiling TB,$(compile-var-deps))

$(call dump-vars-match,$(compile-var-deps),comp)

cov-arg := $(if $(call equal,$(COV),1),--en_cov,)
wave-arg := $(if $(call equal,$(WAVES),1),--en_wave,)
lsf-arg := $(if $(LSF_CMD),--lsf_cmd="$(LSF_CMD)",)

$(OUT)/rtl_sim/.compile.stamp: \
  $(compile-vars-prereq) $(all-verilog) $(risc-dv-files) \
  sim.py yaml/rtl_simulation.yaml \
  | $(OUT)/rtl_sim
	@./sim.py \
	 --o=${OUT} \
	 --steps=compile \
	 ${COMMON_OPTS} \
	 --simulator="${SIMULATOR}" --simulator_yaml=yaml/rtl_simulation.yaml \
	 $(cov-arg) $(wave-arg) --cmp_opts="${COMPILE_OPTS}"
	$(call dump-vars,$(OUT)/rtl_sim/.compile-vars.mk,comp,$(compile-var-deps))
	@touch $@

.PHONY: compile
compile: $(OUT)/rtl_sim/.compile.stamp

###############################################################################
# Run ibex RTL simulation with generated programs
#
# Because we compile a TB once rather than for each seed, we have to copy in
# that directory before we start. We make this step (rather than actually
# running the test) dependent on having the right variables. That way, we'll
# correctly delete the sim directory and re-copy it if necessary.
#
# Note that the variables we depend on are gen-vars-prereq. We also depend on
# COV and WAVES, but these dependencies will come for free from the dependency
# on the compiled TB.
$(metadata)/rtl_sim.compile.stamp: \
  $(gen-vars-prereq) $(risc-dv-files) $(OUT)/rtl_sim/.compile.stamp
	rm -rf $(OUT-SEED)/rtl_sim
	cp -r $(OUT)/rtl_sim $(OUT-SEED)
	@touch $@

# This rule actually runs the simulation. It depends on the copied-in testbench
# and also on us having already compiled the test programs.
$(metadata)/rtl_sim.run.stamp: \
  $(metadata)/rtl_sim.compile.stamp \
  $(metadata)/instr_gen.compile.stamp $(TESTLIST) \
  sim.py yaml/rtl_simulation.yaml
	@./sim.py \
	 --o=$(OUT-SEED) \
	 --steps=sim \
	 ${TEST_OPTS} \
	 --simulator="${SIMULATOR}" --simulator_yaml=yaml/rtl_simulation.yaml \
	 $(cov-arg) $(wave-arg) $(lsf-arg) \
	 --sim_opts="+signature_addr=${SIGNATURE_ADDR} ${SIM_OPTS}"
	@touch $@

.PHONY: rtl_sim
rtl_sim: $(metadata)/rtl_sim.run.stamp

###############################################################################
# Compare ISS and RTL sim results
$(OUT-SEED)/regr.log: \
  $(metadata)/instr_gen.iss.stamp \
  $(metadata)/rtl_sim.run.stamp $(TESTLIST)
	@rm -f $@
	@./sim.py \
     --o=$(OUT-SEED) \
     --steps=compare \
     ${TEST_OPTS} \
     --simulator="${SIMULATOR}" \
     --iss="${ISS}"

.PHONY: post_compare
post_compare: $(OUT-SEED)/regr.log

###############################################################################
# Generate RISCV-DV functional coverage
fcov:
	python3 ${GEN_DIR}/cov.py \
          --core ibex \
          --dir ${OUT}/rtl_sim \
          -o ${OUT}/fcov \
          --isa rv32imc \
          --custom_target riscv_dv_extension

# Merge all output coverage directories into the <out>/rtl_sim directory
cov:
	@rm -rf ${OUT}/rtl_sim/test.vdb
	@./sim.py \
		--steps=cov \
		${TEST_OPTS} \
		--simulator="${SIMULATOR}" \
		--o="${OUT}" \
		--lsf_cmd="${LSF_CMD}";
	@if [ -d "test.vdb" ]; then \
		mv -f test.vdb ${OUT}/rtl_sim/; \
	fi
